Passed
Pull Request — master (#20)
by Muhammad Dyas
01:43
created

ActionHandler.newPollOnChange   A

Complexity

Conditions 1

Size

Total Lines 5
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
1
import {chat_v1 as chatV1} from 'googleapis/build/src/apis/chat/v1';
2
import BaseHandler from './BaseHandler';
3
import NewPollFormCard from '../cards/NewPollFormCard';
4
import {addOptionToState, getConfigFromInput, getStateFromCard} from '../helpers/state';
5
import {callMessageApi} from '../helpers/api';
6
import {createDialogActionResponse, createStatusActionResponse} from '../helpers/response';
7
import PollCard from '../cards/PollCard';
8
import {ClosableType, MessageDialogConfig, PollFormInputs, PollState, TaskEvent, Voter} from '../helpers/interfaces';
9
import AddOptionFormCard from '../cards/AddOptionFormCard';
10
import {saveVotes} from '../helpers/vote';
11
import {PROHIBITED_ICON_URL} from '../config/default';
12
import ClosePollFormCard from '../cards/ClosePollFormCard';
13
import MessageDialogCard from '../cards/MessageDialogCard';
14
import {createTask} from '../helpers/task';
15
import ScheduleClosePollFormCard from '../cards/ScheduleClosePollFormCard';
16
17
/*
18
This list methods are used in the poll chat message
19
 */
20
interface PollAction {
21
  saveOption(): Promise<chatV1.Schema$Message>;
22
23
  getEventPollState(): PollState;
24
}
25
26
export default class ActionHandler extends BaseHandler implements PollAction {
27
  async process(): Promise<chatV1.Schema$Message> {
28
    const action = this.event.common?.invokedFunction;
29
    switch (action) {
30
      case 'start_poll':
31
        return await this.startPoll();
32
      case 'vote':
33
        return this.recordVote();
34
      case 'add_option_form':
35
        return this.addOptionForm();
36
      case 'add_option':
37
        return await this.saveOption();
38
      case 'show_form':
39
        const pollForm = new NewPollFormCard({topic: '', choices: []}, this.getUserTimezone()).create();
40
        return createDialogActionResponse(pollForm);
41
      case 'new_poll_on_change':
42
        return this.newPollOnChange();
43
      case 'close_poll_form':
44
        return this.closePollForm();
45
      case 'close_poll':
46
        return await this.closePoll();
47
      case 'schedule_close_poll_form':
48
        return this.scheduleClosePollForm();
49
      case 'schedule_close_poll':
50
        return this.scheduleClosePoll();
51
      default:
52
        return createStatusActionResponse('Unknown action!', 'UNKNOWN');
53
    }
54
  }
55
56
  /**
57
   * Handle the custom start_poll action.
58
   *
59
   * @returns {object} Response to send back to Chat
60
   */
61
  async startPoll(): Promise<chatV1.Schema$Message> {
62
    // Get the form values
63
    const formValues: PollFormInputs = this.event.common!.formInputs! as PollFormInputs;
64
    const config = getConfigFromInput(formValues);
65
66
    if (!config.topic || config.choices.length === 0) {
67
      // Incomplete form submitted, rerender
68
      const dialog = new NewPollFormCard(config, this.getUserTimezone()).create();
69
      return createDialogActionResponse(dialog);
70
    }
71
72
    if (config.closedTime) {
73
      // because previously in the form, we marked up the time with user timezone offset
74
      const utcClosedTime = config.closedTime + this.getUserTimezone()?.offset ?? 0;
75
      if (utcClosedTime < Date.now() - 360000) {
76
        const dialog = new NewPollFormCard(config, this.getUserTimezone()).create();
77
        return createDialogActionResponse(dialog);
78
      }
79
      config.closedTime = utcClosedTime;
80
    }
81
82
    const pollCardMessage = new PollCard({author: this.event.user, ...config},
83
      this.getUserTimezone()).createMessage();
84
    const request = {
85
      parent: this.event.space?.name,
86
      requestBody: pollCardMessage,
87
    };
88
89
    const apiResponse = await callMessageApi('create', request);
90
    if (apiResponse.data?.name) {
91
      if (config.autoClose && config.closedTime) {
92
        const taskPayload: TaskEvent = {'id': apiResponse.data.name, 'action': 'close_poll', 'type': 'TASK'};
93
        await createTask(JSON.stringify(taskPayload), config.closedTime);
94
        if (config.autoMention) {
95
          const taskPayload: TaskEvent = {'id': apiResponse.data.name, 'action': 'remind_all', 'type': 'TASK'};
96
          await createTask(JSON.stringify(taskPayload), config.closedTime - 420000);
97
        }
98
      }
99
      return createStatusActionResponse('Poll started.', 'OK');
100
    } else {
101
      return createStatusActionResponse('Failed to start poll.', 'UNKNOWN');
102
    }
103
  }
104
105
  /**
106
   * Handle the custom vote action. Updates the state to record
107
   * the user's vote then rerenders the card.
108
   *
109
   * @returns {object} Response to send back to Chat
110
   */
111
  recordVote() {
112
    const parameters = this.event.common?.parameters;
113
    if (!(parameters?.['index'])) {
114
      throw new Error('Index Out of Bounds');
115
    }
116
    const choice = parseInt(parameters['index']);
117
    const userId = this.event.user?.name ?? '';
118
    const userName = this.event.user?.displayName ?? '';
119
    const voter: Voter = {uid: userId, name: userName};
120
    const state = this.getEventPollState();
121
122
    // Add or update the user's selected option
123
    state.votes = saveVotes(choice, voter, state.votes!, state.anon);
124
    const card = new PollCard(state, this.getUserTimezone());
125
    return {
126
      thread: this.event.message?.thread,
127
      actionResponse: {
128
        type: 'UPDATE_MESSAGE',
129
      },
130
      cardsV2: [card.createCardWithId()],
131
    };
132
  }
133
134
  /**
135
   * Opens and starts a dialog that allows users to add details about a contact.
136
   *
137
   * @returns {object} open a dialog.
138
   */
139
  addOptionForm() {
140
    const state = this.getEventPollState();
141
    const dialog = new AddOptionFormCard(state).create();
142
    return createDialogActionResponse(dialog);
143
  };
144
145
  /**
146
   * Handle add new option input to the poll state
147
   * the user's vote then rerenders the card.
148
   *
149
   * @returns {object} Response to send back to Chat
150
   */
151
  async saveOption(): Promise<chatV1.Schema$Message> {
152
    const userName = this.event.user?.displayName ?? '';
153
    const state = this.getEventPollState();
154
    const formValues = this.event.common?.formInputs;
155
    const optionValue = formValues?.['value']?.stringInputs?.value?.[0]?.trim() || '';
156
    addOptionToState(optionValue, state, userName);
157
158
    const cardMessage = new PollCard(state, this.getUserTimezone()).createMessage();
159
160
    const request = {
161
      name: this.event.message!.name,
162
      requestBody: cardMessage,
163
      updateMask: 'cardsV2',
164
    };
165
    const apiResponse = await callMessageApi('update', request);
166
    if (apiResponse.status === 200) {
167
      return createStatusActionResponse('Option is added', 'OK');
168
    } else {
169
      return createStatusActionResponse('Failed to add option.', 'UNKNOWN');
170
    }
171
  }
172
173
  getEventPollState(): PollState {
174
    const stateJson = getStateFromCard(this.event);
175
    if (!stateJson) {
176
      throw new ReferenceError('no valid state in the event');
177
    }
178
    return JSON.parse(stateJson);
179
  }
180
181
  async closePoll(): Promise<chatV1.Schema$Message> {
182
    const state = this.getEventPollState();
183
    state.closedTime = Date.now();
184
    state.closedBy = this.event.user?.displayName ?? '';
185
    const cardMessage = new PollCard(state, this.getUserTimezone()).createMessage();
186
    const request = {
187
      name: this.event.message!.name,
188
      requestBody: cardMessage,
189
      updateMask: 'cardsV2',
190
    };
191
    const apiResponse = await callMessageApi('update', request);
192
    if (apiResponse.status === 200) {
193
      return createStatusActionResponse('Poll is closed', 'OK');
194
    } else {
195
      return createStatusActionResponse('Failed to close poll.', 'UNKNOWN');
196
    }
197
  }
198
199
  closePollForm() {
200
    const state = this.getEventPollState();
201
    if (state.type === ClosableType.CLOSEABLE_BY_ANYONE || state.author!.name === this.event.user?.name) {
202
      return createDialogActionResponse(new ClosePollFormCard(state, this.getUserTimezone()).create());
203
    }
204
205
    const dialogConfig: MessageDialogConfig = {
206
      title: 'Sorry, you can not close this poll',
207
      message: `The poll setting restricts the ability to close the poll to only the creator(${state.author!.displayName}).`,
208
      imageUrl: PROHIBITED_ICON_URL,
209
    };
210
    return createDialogActionResponse(new MessageDialogCard(dialogConfig).create());
211
  }
212
213
  async scheduleClosePoll(): Promise<chatV1.Schema$Message> {
214
    const formValues: PollFormInputs = this.event.common!.formInputs! as PollFormInputs;
215
    const config = getConfigFromInput(formValues);
216
217
    // because previously in the form, we marked up the time with user timezone offset
218
    const utcClosedTime = config.closedTime! + this.getUserTimezone()?.offset ?? 0;
219
    if (utcClosedTime < Date.now() - 360000) {
220
      const dialog = new ScheduleClosePollFormCard(config, this.getUserTimezone()).create();
221
      return createDialogActionResponse(dialog);
222
    }
223
    config.closedTime = utcClosedTime;
224
    const messageId = this.event.message!.name!;
225
    const taskPayload: TaskEvent = {'id': messageId, 'action': 'close_poll', 'type': 'TASK'};
226
    await createTask(JSON.stringify(taskPayload), config.closedTime);
227
    if (config.autoMention) {
228
      const taskPayload: TaskEvent = {'id': messageId, 'action': 'remind_all', 'type': 'TASK'};
229
      await createTask(JSON.stringify(taskPayload), config.closedTime - 420000);
230
    }
231
    const state = this.getEventPollState();
232
    state.closedTime = utcClosedTime;
233
    const cardMessage = new PollCard(state, this.getUserTimezone()).createMessage();
234
235
    const request = {
236
      name: this.event.message!.name,
237
      requestBody: cardMessage,
238
      updateMask: 'cardsV2',
239
    };
240
    const apiResponse = await callMessageApi('update', request);
241
    if (apiResponse.status === 200) {
242
      return createStatusActionResponse('Poll is scheduled to close', 'OK');
243
    } else {
244
      return createStatusActionResponse('Failed to schedule close poll.', 'UNKNOWN');
245
    }
246
  }
247
248
  scheduleClosePollForm() {
249
    const state = this.getEventPollState();
250
    return createDialogActionResponse(new ScheduleClosePollFormCard(state, this.getUserTimezone()).create());
251
  }
252
253
  newPollOnChange() {
254
    const formValues: PollFormInputs = this.event.common!.formInputs! as PollFormInputs;
255
    const config = getConfigFromInput(formValues);
256
    return createDialogActionResponse(new NewPollFormCard(config, this.getUserTimezone()).create());
257
  }
258
}
259